#include "io/vsServer.h"
#include "io/vsNetTasks.h"
#include "io/vsFileOps.h"

#include <assert.h>

#include <QFile>
#include <QFileInfo>
#include <QTimer>

const QString c_static_served_files_path = ":/files/res/";

// needs args: 1) title 2) id 3) width 4) height 5) url
const QString c_embed_html_string = "<!DOCTYPE html><html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" /><title>VocaTube :: %1</title></head><body><div id=\"%2\" class=\"mjs-box\" style=\"width:%3px; height:%4px;\" autoplay><script src=\"%5embed.js\"></script></div></body></html>";


inline QStringPair genServerDates(int expirationOffset = 100) // offset in seconds
{
    QStringPair result;

    QDateTime t(QDateTime::currentDateTimeUtc());
    QLocale   l(QLocale::English, QLocale::UnitedStates);

    //                      Wed, 26 Sep 2012 13:44:50 GMT
    result.first  = QString("%1, %2 %3 %4 %5 GMT")
                            .arg(l.dayName(t.date().dayOfWeek(), QLocale::ShortFormat))
                            .arg(t.date().day())
                            .arg(l.monthName(t.date().month(), QLocale::ShortFormat))
                            .arg(t.date().year())
                            .arg(t.time().toString());

    t = t.addSecs(expirationOffset);

    result.second = QString("%1, %2 %3 %4 %5 GMT")
                            .arg(l.dayName(t.date().dayOfWeek(), QLocale::ShortFormat))
                            .arg(t.date().day())
                            .arg(l.monthName(t.date().month(), QLocale::ShortFormat))
                            .arg(t.date().year())
                            .arg(t.time().toString());

    return result;
}


vsServer::vsServer(vsInfoController *infc, QObject *parent)
    : QTcpServer(parent), upLimit(0), dlLimit(0), localPort(0), listening(false), cheduledTransfer(false), ic(infc)
{
    mimes["json"] = "application/json; charset=utf-8";
    mimes["html"] = "text/html; charset=utf-8";
    mimes["ico"]  = "image/x-icon";
    mimes["js"]   = "application/x-javascript; charset=utf-8";
    mimes["css"]  = "text/css; charset=utf-8";
    mimes["png"]  = "image/png";
    mimes["txt"]  = "text/plain";

    server_url = "http://localhost/";
}

void vsServer::stop()
{
    vsLog::i("Stopping server...");
    disconnect(this, SIGNAL(newConnection()), this, SLOT(onNewConnection()));

    // stop all serve tasks
    while (!fileServeTasks.isEmpty())
    {
        vsFileServeTask* fst = fileServeTasks.first();
        fst->stop();
        delete fileServeTasks.first();
        fileServeTasks.removeFirst();
    }

    listening = false;
    close();
}

bool vsServer::startAtPort(int port)
{
    // start server
    if (listen(QHostAddress::Any, port))
    {
        localPort = port;
        vsLog::i(QString("%1 %2").arg(tr("Server successfully started at port")).arg(port));
        connect(this, SIGNAL(newConnection()), this, SLOT(onNewConnection()));
        listening = true;
    }
    else
        vsLog::e(QString("Could not start server on port %1").arg(port));

    return listening;
}

vsServer::~vsServer()
{
    stop();
    vsLog::d("Server: shutting down");
}

void vsServer::setMaxUploadRate(int bytes_per_second)
{
    upLimit = bytes_per_second;
}

void vsServer::setMaxDownloadRate(int bytes_per_second)
{
    dlLimit = bytes_per_second;
}

void vsServer::onNewConnection()
{
    while (hasPendingConnections())
    {
        QTcpSocket *s = nextPendingConnection();

        connect(s, SIGNAL(readyRead()), this, SLOT(onSocketReady()));
        connect(s, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onSocketError()));
    }
}

inline bool parseRangeValue(const QString &value, qint64 &left, qint64 &right)
{
    bool result = false;
    QString trimmedValue = value.trimmed();

    if (!trimmedValue.isEmpty() && trimmedValue.left(6) == "bytes=")
    {
        QStringList rangeParts = trimmedValue.remove(0,6).split('-');

        if (rangeParts.length() > 1)
        {
            left  = rangeParts[0].isEmpty() ? -1 : rangeParts[0].toLongLong();
            right = rangeParts[1].isEmpty() ? -1 : rangeParts[1].toLongLong();

            result = true;
        }
    }

    return result;
}

void vsServer::onSocketReady()
{
    bool ok = false;

    QTcpSocket *socket = dynamic_cast<QTcpSocket*>(sender());
    assert(socket != 0);

    vsNetHeader h(socket);

    switch (h.type)
    {
    case vsTypes::http_get:
        {
            if (h.requestURL == "/" && !h.requestBody.isEmpty()
                    && QFileInfo(c_static_served_files_path + h.requestBody).exists())
                ok = serveStatic(&h, socket, h.requestBody);
            else
            {
                bool vocaseed = h.requestBody.contains(".vs");                                  // just serve media file
                bool html     = h.requestBody.mid(h.requestBody.lastIndexOf('.')) == ".html";   // serve html with embedded player
                bool json     = h.requestBody.mid(h.requestBody.lastIndexOf('.')) == ".json";   // serve json data of media file
                bool poster   = h.requestBody.mid(h.requestBody.lastIndexOf('.')) == ".poster"; // serve cover image of media file
                bool robots   = h.requestBody.endsWith("robots.txt");                           // search bots asking for rules

                if (robots)
                    ok = serveRobots(&h, socket);
                else
                {   // media id must not be empty
                    int idLength  = h.requestBody.indexOf('.');
                    QString id    = h.requestBody.left(idLength);
                    bool hasID    = idLength > 0 && ic->canServeID(id);

                    if (vocaseed && hasID)   // requesting vocaseed service exactly
                    {
                        if (html)
                            ok = serveHTML  (&h, socket, id);
                        else if (json)
                            ok = serveJSON  (&h, socket, id);
                        else if (poster)
                            ok = servePoster(&h, socket, id);
                        else
                            ok = serveMedia (&h, socket, id);
                    }
                }
            }
        }
        break;
    case vsTypes::http_head:    // proved to have numerous issues on practice :(
    case vsTypes::p2p:          // todo in some bright future
    case vsTypes::default_header:
    default:
        break;
    }

    if (!ok)
    {
        QString msg(tr("Wrong request."));
        QString wit;
        int roll = (double)rand() / RAND_MAX * 100.f; // when in doubt, roll d20

        if (roll < c_wit_percent)
            wit = wittyServeRefusal();

        socket->write(c_no_content_response.toLatin1() + "\r\n");

        if (!wit.isEmpty())
        {
            msg.append(" ").append(wit);
            socket->write("Comment: \"" + wit.toLatin1() + "\"\r\n\r\n");
        }
        else
            socket->write("\r\n");

        vsLog::e(msg);

        socket->flush();
        socket->close();
        socket->deleteLater();
    }
}

bool vsServer::serveMedia(vsNetHeader* h, QTcpSocket *s, QString id)
{
    assert(h != 0);
    assert(s != 0);
    bool ok = true;

    vsNetworkMediaInfo i;
    i.id = id;

    if (ic->getMediaInfo(i))
    {
        bool   rangedServe = false;
        qint64 offset = 0;
        qint64 count  = i.size;

        if (h->headers.contains("Range"))
        {
            ok = false; // let's check the range first
            qint64 left = 0;
            qint64 right = 0;

            if (parseRangeValue(h->headers["Range"], left, right))
            {
                if (left == -1)  left = 0;
                if (right == -1) right = i.size - 1; // -1 since it's index and size is count

                // 12.01.2013 fix: treating right as index, too, and allowing requesting of 1 byte (fixing Opera's issue)
                if (left <= right && left >= 0 && right >= 0 && left < i.size && right < i.size)
                {
                    ok = true;
                    rangedServe = true;
                    offset = left;
                    count  = right - left + 1; // +1 since both are indexes, so 1) can have same value 2) both are included
                }
                else vsLog::e("Wrong range " + h->headers["Range"]);
            }
            else vsLog::e("Could not parse range " + h->headers["Range"]);
        }

        if (!ok)
            return false; //-----------------------------------------------------------------------------

        h->headers.clear();

        if (rangedServe)
        {
            h->response = c_partial_response;
            h->headers["Content-Range"] = QString("bytes %1-%2/%3").arg(offset).arg(offset+count-1).arg(i.size); // -1 because both are indexes
        }
        else
            h->response = c_single_batch_response;

        h->headers["Cache-Control"] = c_cache_control;
        h->headers["Content-Type"]  = i.mime;
        h->headers["Content-Length"]= QString("%1").arg(count);
        h->headers["Connection"]    = "keep-alive";

        h->headers["Content-Disposition"] = QString("inline; filename*=utf-8''%1").arg(i.tags["filename"]);
        h->headers["X-Content-Duration"]  = QString("%1").arg(i.duration/1000.f);

        QStringPair dates = genServerDates((double)i.duration / 500.f); // x2

        h->headers["Date"]    = dates.first;
        h->headers["Expires"] = dates.second;
        h->headers["Server"]  = "VocaSeed";

        *s << *h;   // writing header to socket (sending)
        s->flush();

        disconnect(s, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onSocketError()));
        disconnect(s, SIGNAL(readyRead()), this, SLOT(onSocketReady()));

        vsLog::i(QString("%1 %2 @ %3-%4/%5").arg(tr("Serving")).arg(i.path).arg(offset).arg(offset+count-1).arg(i.size));

        vsFileServeTask *fst = new vsFileServeTask(*s, i.path, offset, count, i.size, this);
        s  ->setObjectName(i.id);
        fst->setObjectName(i.id);

        connect(s,   SIGNAL(bytesWritten(qint64)), this, SLOT(onSocketBytesSent(qint64)));
        connect(fst, SIGNAL(error()),              this, SLOT(onFileServeTaskError()));
        connect(fst, SIGNAL(transferComplete()),   this, SLOT(onTransferComplete()));

        fileServeTasks.append(fst);
        cheduleTransfer();
    }
    else
    {
        vsLog::e(tr("Requesting media info failed."));
        ok = false;
    }

    return ok;
}


QByteArray tagsToJSON(const QStringMap tags)
{
    QByteArray a;
    a.append("{\r\n");

    for (QStringMap::const_iterator i = tags.begin(); i != tags.end(); ++i)
        a.append(QString("\t\"%1\": \"%2\",\r\n").arg(i.key()).arg(i.value()).toUtf8());

    a.append("}\r\n");
    return a;
}


bool vsServer::serveJSON(vsNetHeader *h, QTcpSocket *s, QString id)
{
    assert(h != 0);
    assert(s != 0);

    bool ok = true;
    vsNetworkMediaInfo i;
    i.id = id;

    if (ic->getMediaInfo(i))
    {
        i.tags["source"] = server_url + id + ".vs";
        QByteArray jsonData = tagsToJSON(i.tags);

        h->response = c_single_batch_response;

        h->headers.clear();
        h->headers["Cache-Control"] = c_cache_control;
        h->headers["Content-Type"]  = mimes["json"];
        h->headers["Content-Length"]= QString("%1").arg(jsonData.length());
        h->headers["Connection"]    = "close";

        QStringPair dates = genServerDates();

        h->headers["Date"]    = dates.first;
        h->headers["Expires"] = dates.second;
        h->headers["Server"]  = "VocaSeed";

        *s << *h;               // writing header to socket
        s->write(jsonData);     // writing json as body
        s->flush();
    }
    else
    {
        vsLog::e(tr("Requesting media info failed."));
        ok = false;
    }

    return ok;
}


bool vsServer::serveHTML(vsNetHeader *h, QTcpSocket *s, QString id)
{
    assert(h != 0);
    assert(s != 0);
    assert(!id.isEmpty());

    QByteArray html = c_embed_html_string.arg(id).arg(id + ".vs").arg(400).arg(250).arg(server_url).toUtf8();

    h->response = c_single_batch_response;

    h->headers.clear();
    h->headers["Cache-Control"] = c_cache_control;
    h->headers["Content-Type"]  = mimes["html"];
    h->headers["Content-Length"]= QString("%1").arg(html.size());
    h->headers["Connection"]    = "close";

    QStringPair dates = genServerDates();

    h->headers["Date"]    = dates.first;
    h->headers["Expires"] = dates.second;
    h->headers["Server"]  = "VocaSeed";

    *s << *h;
    s->write(html);
    s->flush();

    return true;
}


bool vsServer::serveRobots(vsNetHeader *h, QTcpSocket *s)
{
    assert(h != 0);
    assert(s != 0);

    h->response = c_single_batch_response;

    h->headers.clear();
    h->headers["Cache-Control"] = c_cache_control;
    h->headers["Content-Type"]  = mimes["txt"];
    h->headers["Content-Length"]= QString("%1").arg(c_robots_go_away.size());
    h->headers["Connection"]    = "close";
    h->headers["Server"]        = "VocaSeed";

    *s << *h;
    s->write(c_robots_go_away.toUtf8());
    s->flush();

    return true;
}


/** Static file should be in app's resources (compiled in or attached in runtime)
        to avoid vulnerabilities with access to filesystem */
bool vsServer::serveStatic(vsNetHeader *h, QTcpSocket *s, QString name)
{
    assert(h != 0);
    assert(s != 0);
    assert(!name.isEmpty());

    name.prepend(c_static_served_files_path);
    QFileInfo fi(name);

    if (!(fi.exists() && fi.isReadable() && !fi.suffix().isEmpty()))
    {
        vsLog::e("Static file unavailable: " + name);
        return false;
    }

    if (!mimes.contains(fi.suffix()))
    {
        vsLog::e("No MIME type for extension: " + fi.suffix());
        return false;
    }

    h->response = c_single_batch_response;

    h->headers.clear();
    h->headers["Cache-Control"] = c_cache_control;
    h->headers["Content-Type"]  = mimes[fi.suffix()];
    h->headers["Content-Length"]= QString("%1").arg(fi.size());
    h->headers["Connection"]    = "close";

    QStringPair dates = genServerDates();

    h->headers["Date"]    = dates.first;
    h->headers["Expires"] = dates.second;
    h->headers["Server"]  = "VocaSeed";

    *s << *h;                         // writing header to socket

    QFile f(name);
    assert(f.open(QFile::ReadOnly));
    s->write(f.readAll());
    f.close();

    s->flush();

    return true;
}

bool vsServer::servePoster(vsNetHeader *h, QTcpSocket *s, QString id)
{
    assert(h != 0);
    assert(s != 0);

    bool ok = true;
    vsNetworkMediaInfo i;
    i.id = id;

    if (ic->getMediaInfo(i))
    {
        if (i.poster.isEmpty())
            ok = serveStatic(h, s, "default_poster.png"); // no poster :(
        else
        {
            h->response = c_single_batch_response;

            h->headers.clear();
            h->headers["Cache-Control"] = c_cache_control;
            h->headers["Content-Type"]  = i.posterMime;
            h->headers["Content-Length"]= QString("%1").arg(i.poster.size());
            h->headers["Connection"]    = "close";

            QStringPair dates = genServerDates();

            h->headers["Date"]    = dates.first;
            h->headers["Expires"] = dates.second;
            h->headers["Server"]  = "VocaSeed";

            *s << *h;               // writing header to socket
            s->write(i.poster);     // writing image as body
            s->flush();
        }
    }
    else
    {
        vsLog::e(tr("Requesting media info failed."));
        ok = false;
    }

    return ok;
}

void vsServer::onSocketBytesSent(qint64 count)
{
    if (count > 0 && !sender()->objectName().isEmpty())
        emit transmitted(sender()->objectName(), count);
}

void vsServer::onSocketError()
{
    QTcpSocket *s = dynamic_cast<QTcpSocket*>(sender());

    if (!(s == 0 || s->objectName().isEmpty()))
    {
        s->close(); // whatever the reason was
        s->deleteLater();
    }
}


void vsServer::onFileServeTaskError()
{
    vsFileServeTask *fst = dynamic_cast<vsFileServeTask*>(sender());

    if (fst != 0)
    {
        emit transmitted(fst->objectName(), c_clear_byterate_stats); // signal to clear byterate stats

        fileServeTasks.removeAll(fst);
        delete fst;
    }
}


void vsServer::onTransferComplete()
{
    vsFileServeTask *fst = dynamic_cast<vsFileServeTask*>(sender());

    if (fst != 0)
    {
        emit transmitted(fst->objectName(), c_clear_byterate_stats); // signal to clear byterate stats

        fileServeTasks.removeAll(fst);
        delete fst;
    }
}


void vsServer::cheduleTransfer()
{
    if (!(cheduledTransfer || fileServeTasks.isEmpty()))
        QTimer::singleShot(50, this, SLOT(transfer()));
}


void vsServer::transfer()
{
    if (!fileServeTasks.isEmpty())
        foreach(vsFileServeTask *fst, fileServeTasks)
            fst->transfer(500000); // 80 Mbit/s

    cheduledTransfer = false;
    cheduleTransfer();
}

void vsServer::onURLChanged(QString new_url)
{
    if (!new_url.isEmpty())
        server_url = new_url;
    else
        vsLog::e("New server url is empty! Using previous one.");
}

/*

    // request -----------------------------------------------------------
    QNetworkAccessManager *manager = new QNetworkAccessManager(this);
    connect(manager, SIGNAL(finished(QNetworkReply*)),
           this, SLOT(replyFinished(QNetworkReply*)));

    manager->get(QNetworkRequest(QUrl("http://localhost:8000/i123")));

    // utf plaintext codes to qchars -------------------------------------
    QString str(reply->readAll());
    QRegExp rx("(\\\\u[0-9a-fA-F]{4})");
    int pos = 0;

    while ((pos = rx.indexIn(str, pos)) != -1)
        str.replace(pos++, 6, QChar(rx.cap(1).right(4).toUShort(0, 16)));
*/
